package com.gitee.wsl.common.ui.ext.color

import androidx.compose.ui.graphics.Color
import com.gitee.wsl.mathematics.interpolator.Interpolate
import com.gitee.wsl.math.interpolator.basis
import com.gitee.wsl.math.interpolator.basisClosed
import com.gitee.wsl.mathematics.interpolator.Percent
import com.gitee.wsl.mathematics.interpolator.PercentOrRatio
import kotlin.math.PI
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.sqrt


private const val pi_1_3 = PI / 3.0
private const val pi_2_3 = PI * 2.0 / 3.0

// TODO no more constant needed ?
// = interpolate.color.gamma & interpolate.color.nogamma in D3
internal fun Color.Companion.gamma(gamma: Double = 1.0): (Double, Double) -> Interpolate<Double> {
    return { a, b -> if (gamma == 1.0) linearClamped(a, b - a) else exponential(a, b, gamma) }
}


// TODO : see if needed (gamma needs rework)
//internal fun ungamma(y: Double = 1.0): (Double, Double) -> (Double) -> Double {
//    return { a, b -> if (y == 1.0)
//        uninterpolateNumber(a, b)
//    else
//        exponential(a, b, y)            // TODO unexponential ??
//    }
//}

/**
 * Hue interpolation, take the shortest path between 2 hues if 'long' is not set to true.
 */
// TODO : interpolator<Angle> ?
/*internal fun Interpolate.Companion.interpolateHue(from: AngleUnit, to: AngleUnit, long: Boolean = false): InterpolatorFun<Double> {
    val a2 = from.normalize()
    val b2 = to.normalize()
    val diff = b2.rad - a2.rad
    return { t ->
        when {
            !long && diff < -PI -> linearClamped(a2.rad, diff + TAU)(t)
            !long && diff > PI -> linearClamped(a2.rad, diff - TAU)(t)
            else -> linearClamped(a2.rad, diff)(t)
        }
    }
}*/

/**
 * Clamped linear interpolation
 */
// TODO : note that this function is clamped, so we can't access a color outside of the range (ex. for asking 110%)
private fun Color.Companion.linearClamped(a: Double, b: Double): Interpolate<Double> = Interpolate{ t -> a + t.coerceToDefault().value * b }

/*private fun linearClamped(values: List<Number>): (Double) -> Double {
    val n = values.size - 1
    return fun(t: Double): Double {
        val currentIndex: Int = if (t <= 0) 0 else if (t >= 1) n - 1 else Math.floor(t * n)
        val newT = t.coerceIn(0.0, 1.0)

        val t1 = (newT - currentIndex.toDouble() / n) * n
        return values[currentIndex].toDouble() * (1.0 - t1) + values[currentIndex + 1].toDouble() * t1
    }
}*/

/**
 * exponential interpolation
 */
private fun Color.Companion.exponential(a: Double, b: Double, y: Double): Interpolate<Double> {
    val ny = 1 / y
    val na = a.pow(y)
    val nb = b.pow(y) - na

    return Interpolate{t-> (na + t.value * nb).pow(ny)}
}
/*private fun exponential(values: List<Number>, y: Double): (Double) -> Double {
    val ny = 1 / y
    val n = values.size - 1

    return fun(t): Double {
        val currentIndex: Int = if (t <= 0) 0 else if (t >= 1) n - 1 else Math.floor(t * n)

        val na = Math.pow(values[currentIndex].toDouble(), y)
        val nb = Math.pow(values[currentIndex + 1].toDouble(), y) - na

        return Math.pow(na + t * nb, ny)
    }
}*/

 fun Color.Companion.getSplineInterpolator(cyclical: Boolean): (List<Int>) -> Interpolate<Double> =
    when {
        cyclical -> { a -> Interpolate.basisClosed(a) }
        else ->     { a -> Interpolate.basis(a) }
    }


// TODO List instead of start, end ? (validate and check size !!)
// TODO rename interpolate
// TODO : check colors interpolation from chroma.js
// TODO : extractgamma function (check D3 last version)
private fun Color.Companion.interpolateRgb(start: Color, end: Color, gamma: Double = 1.0): Interpolate<Color> {
    val interpolator = gamma(gamma)

    val r = interpolator(start.r.toDouble(), end.r.toDouble())
    val g = interpolator(start.g.toDouble(), end.g.toDouble())
    val b = interpolator(start.b.toDouble(), end.b.toDouble())
    val a = interpolator(start.alpha.toDouble(), end.alpha.toDouble())

    return Interpolate{percent-> Color.rgb(
        r(percent).roundToInt(),
        g(percent).roundToInt(),
        b(percent).roundToInt(),
        Percent(a(percent))
    )}
}

private fun Color.Companion.lRGBInterpolator(start: Double, end: Double): Interpolate<Double> = Interpolate{
    val percent = it.coerceToDefault()
    sqrt(start.pow(2) * (1 - percent.value) + end.pow(2) * percent.value)
}


/**
 * lRGB interpolation produce a better result when mixing 2 colors (right "luminance" = perceived lightness of the
 * color) by using pow(2) * versions of RGB channels.
 * For more information check this cool short video: https://www.youtube.com/watch?v=LKnqECcg6Gw
 */
// TODO alpha
private fun Color.Companion.interpolateLRgb(start: Color, end: Color): Interpolate<Color> {

    val r = lRGBInterpolator(start.r.toDouble(), end.r.toDouble())
    val g = lRGBInterpolator(start.g.toDouble(), end.g.toDouble())
    val b = lRGBInterpolator(start.b.toDouble(), end.b.toDouble())

   return Interpolate { percent ->
        Color.rgb(
             r(percent).roundToInt(),
             g(percent).roundToInt(),
             b(percent).roundToInt()
        )
    }
}

// TODO add alpha interpolation (alpha is linear not spline ?)
private fun Color.Companion.interpolateRgbBasis(colorsList: List<Color>, cyclical: Boolean = false): Interpolate<Color> {
    val spline = getSplineInterpolator(cyclical)

    val r = spline(colorsList.map { it.r })
    val g = spline(colorsList.map { it.g })
    val b = spline(colorsList.map { it.b })

    return Interpolate{percent-> Color.rgb(
        r(percent).roundToInt(),
        g(percent).roundToInt(),
        b(percent).roundToInt()
    )}
}

private fun percentToSinebow(percent: PercentOrRatio) : Color {
    val t = (0.5 - percent.value) * PI
    var x = kotlin.math.sin(t)
    val r = (255 * x * x).roundToInt()
    x = kotlin.math.sin(t + pi_1_3)
    val g = (255 * x * x).roundToInt()
    x = kotlin.math.sin(t + pi_2_3)
    val b = (255 * x * x).roundToInt()
    return Color.rgb(r,g,b)
}

 fun Color.Companion.rgbBasisInterpolator(colors: List<Color>) = interpolateRgbBasis(colors, false)

 fun Color.Companion.rgbBasisClosedInterpolator(colors: List<Color>) = interpolateRgbBasis(colors, true)

 fun Color.Companion.rgbDefaultInterpolator(start: Color, end: Color)= interpolateRgb(start, end)

 fun Color.Companion.rgbLinearInterpolator(start: Color, end: Color) = interpolateLRgb(start, end)

 fun Color.Companion.rgbSineBowInterpolator() = ::percentToSinebow

fun Color.Companion.rgbaInterpolator(startValue: Color,
                                     endValue: Color) : Interpolate<Color> {
    val startA = startValue.alpha / 255.0f
    var startR = startValue.red / 255.0f
    var startG = startValue.green / 255.0f
    var startB = startValue.blue / 255.0f
    val endA = endValue.alpha / 255.0f
    var endR = endValue.red / 255.0f
    var endG = endValue.green / 255.0f
    var endB = endValue.blue / 255.0f

    // Convert from sRGB to linear.
    startR = startR.pow(2.2f)
    startG = startG.pow(2.2f)
    startB = startB.pow(2.2f)
    endR = endR.pow(2.2f)
    endG = endG.pow(2.2f)
    endB = endB.pow(2.2f)

    return Interpolate{percent->

        // Compute the interpolated color in linear space.
        var a = startA + percent * (endA - startA)
        var r = startR + percent * (endR - startR)
        var g = startG + percent * (endG - startG)
        var b = startB + percent * (endB - startB)

        // Convert back to sRGB in the [0..255] range.
        a *= 255.0f

        r = r.pow(1.0 / 2.2f) * 255.0f
        g = g.pow(1.0 / 2.2f) * 255.0f
        b = b.pow(1.0 / 2.2f) * 255.0f

        Color.rgb(
            r.roundToInt(),
            g.roundToInt(),
            b.roundToInt(),
            a.toFloat())
    }
}
